/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.exec;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

import javax.swing.event.EventListenerList;

import org.apache.commons.exec.launcher.CommandLauncher;
import org.apache.commons.exec.launcher.CommandLauncherFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The default class to start a subprocess. The implementation allows to
 * <ul>
 * <li>set a current working directory for the subprocess</li>
 * <li>provide a set of environment variables passed to the subprocess</li>
 * <li>capture the subprocess output of stdout and stderr using an
 * ExecuteStreamHandler</li>
 * <li>kill long-running processes using an ExecuteWatchdog</li>
 * <li>define a set of expected exit values</li>
 * <li>terminate any started processes when the main process is terminating
 * using a ProcessDestroyer</li>
 * </ul>
 *
 * The following example shows the basic usage:
 *
 * <pre>
 * Executor exec = new DefaultExecutor();
 * CommandLine cl = new CommandLine("ls -l");
 * int exitvalue = exec.execute(cl);
 * </pre>
 *
 */
public class DefaultExecutor implements Executor {
	private final static Logger LOGGER = LoggerFactory.getLogger(DefaultExecutor.class);
	/** taking care of output and error stream */
	private ProcessStreamHandler streamHandler;

	/** the working directory of the process */
	private File workingDirectory;

	/** monitoring of long running processes */
	// private ExecuteWatchdog watchdog;

	/** the exit values considered to be successful */
	private int[] exitValues;

	/** launches the command in a new process */
	private final CommandLauncher launcher;

	/** optional cleanup of started processes */
	private ProcessDestroyer processDestroyer;

	/** worker thread for asynchronous execution */
	//private Thread executorThread;

	/** the first exception being caught to be thrown to the caller */
	private IOException exceptionCaught;

	private ScheduledExecutorService scheduledExecutorService;

	private boolean useInternalExecutorService=true;
	
	private EventListenerList listeners=new EventListenerList();
	/**
	 * Default constructor creating a default {@code PumpStreamHandler} and sets the
	 * working directory of the subprocess to the current working directory.
	 *
	 * The {@code PumpStreamHandler} pumps the output of the subprocess into our
	 * {@code System.out} and {@code System.err} to avoid into our
	 * {@code System.out} and {@code System.err} to avoid a blocked or deadlocked
	 * subprocess (see{@link java.lang.Process Process}).
	 */
	public DefaultExecutor() {
		this(false);
	}

	public DefaultExecutor(boolean redirectErrorStream) {
		this.streamHandler = new DefaultProcessStreamHandler();
		Properties proerties = new Properties();
		proerties.setProperty("redirectErrorStream", redirectErrorStream + "");
		this.launcher = CommandLauncherFactory.createVMLauncher();
		this.exitValues = new int[0];
		this.workingDirectory = new File(".");
		this.exceptionCaught = null;
	}

	/**
	 * @see org.apache.commons.exec.Executor#getStreamHandler()
	 */
	@Override
	public ProcessStreamHandler getStreamHandler() {
		return streamHandler;
	}

	/**
	 * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ProcessStreamHandler)
	 */
	@Override
	public void setStreamHandler(final ProcessStreamHandler streamHandler) {
		this.streamHandler = streamHandler;
	}

	/**
	 * @see org.apache.commons.exec.Executor#getWatchdog()
	 */
	/*
	 * @Override public ExecuteWatchdog getWatchdog() { return watchdog; }
	 */

	/**
	 * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog)
	 */
	/*
	 * @Override public void setWatchdog(final ExecuteWatchdog watchDog) {
	 * this.watchdog = watchDog; }
	 */

	/**
	 * @see org.apache.commons.exec.Executor#getProcessDestroyer()
	 */
	@Override
	public ProcessDestroyer getProcessDestroyer() {
		return this.processDestroyer;
	}

	/**
	 * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer)
	 */
	@Override
	public void setProcessDestroyer(final ProcessDestroyer processDestroyer) {
		this.processDestroyer = processDestroyer;
	}

	/**
	 * @see org.apache.commons.exec.Executor#getWorkingDirectory()
	 */
	@Override
	public File getWorkingDirectory() {
		return workingDirectory;
	}

	/**
	 * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File)
	 */
	@Override
	public void setWorkingDirectory(final File dir) {
		this.workingDirectory = dir;
	}

	/**
	 * @see org.apache.commons.exec.Executor#execute(CommandLine)
	 */
	@Override
	public int execute(final CommandLine command) throws ExecuteException, IOException {
		return execute(command, (Map<String, String>) null);
	}

	/**
	 * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map)
	 */
	@Override
	public int execute(final CommandLine command, final Map<String, String> environment) throws ExecuteException, IOException {

		if (workingDirectory != null && !workingDirectory.exists()) {
			throw new IOException(workingDirectory + " doesn't exist.");
		}

		return executeInternal(command, environment, workingDirectory, streamHandler);

	}

	/**
	 * @see org.apache.commons.exec.Executor#execute(CommandLine,
	 *      org.apache.commons.exec.ExecuteResultHandler)
	 */
	@Override
	public void execute(final CommandLine command, final ExecuteResultHandler handler) throws ExecuteException, IOException {
		execute(command, null, handler);
	}

	/**
	 * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map,
	 *      org.apache.commons.exec.ExecuteResultHandler)
	 */
	@Override
	public void execute(final CommandLine command, final Map<String, String> environment, final ExecuteResultHandler handler) throws ExecuteException, IOException {

		if (workingDirectory != null && !workingDirectory.exists()) {
			throw new IOException(workingDirectory + " doesn't exist.");
		}

		// TODO
		/*
		 * if (watchdog != null) { watchdog.setProcessNotStarted(); }
		 */

		final Runnable runnable = new Runnable() {
			@Override
			public void run() {
				int exitValue = Executor.INVALID_EXITVALUE;
				try {
					exitValue = executeInternal(command, environment, workingDirectory, streamHandler);
					handler.onProcessComplete(exitValue);
				} catch (final ExecuteException e) {
					handler.onProcessFailed(e);
				} catch (final Exception e) {
					handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e));
				}
			}
		};

		this.scheduledExecutorService.execute(runnable);
	}

	/** @see org.apache.commons.exec.Executor#setExitValue(int) */
	@Override
	public void setExitValue(final int value) {
		this.setExitValues(new int[] { value });
	}

	/** @see org.apache.commons.exec.Executor#setExitValues(int[]) */
	@Override
	public void setExitValues(final int[] values) {
		this.exitValues = values == null ? null : (int[]) values.clone();
	}

	/** @see org.apache.commons.exec.Executor#isFailure(int) */
	@Override
	public boolean isFailure(final int exitValue) {

		if (this.exitValues == null) {
			return false;
		}
		if (this.exitValues.length == 0) {
			return this.launcher.isFailure(exitValue);
		}
		for (final int exitValue2 : this.exitValues) {
			if (exitValue2 == exitValue) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Creates a process that runs a command.
	 *
	 * @param command
	 *            the command to run
	 * @param env
	 *            the environment for the command
	 * @param dir
	 *            the working directory for the command
	 * @return the process started
	 * @throws IOException
	 *             forwarded from the particular launcher used
	 */
	protected Process launch(final CommandLine command, final Map<String, String> env, final File dir) throws IOException {

		if (this.launcher == null) {
			throw new IllegalStateException("CommandLauncher can not be null");
		}

		if (dir != null && !dir.exists()) {
			throw new IOException(dir + " doesn't exist.");
		}
		return this.launcher.exec(command, env, dir);
	}

	/**
	 * Get the worker thread being used for asynchronous execution.
	 *
	 * @return the worker thread
	 */
	/*protected Thread getExecutorThread() {
		return executorThread;
	}*/

	/**
	 * Close the streams belonging to the given Process.
	 *
	 * @param process
	 *            the <CODE>Process</CODE>.
	 */
	private void closeProcessStreams(final Process process) {

		try {
			process.getInputStream().close();
		} catch (final IOException e) {
			setExceptionCaught(e);
		}

		try {
			process.getOutputStream().close();
		} catch (final IOException e) {
			setExceptionCaught(e);
		}

		try {
			process.getErrorStream().close();
		} catch (final IOException e) {
			setExceptionCaught(e);
		}
		
		if(this.useInternalExecutorService) {
			this.scheduledExecutorService.shutdownNow();
		}
	}

	/**
	 * Execute an internal process. If the executing thread is interrupted while
	 * waiting for the child process to return the child process will be killed.
	 *
	 * @param command
	 *            the command to execute
	 * @param environment
	 *            the execution environment
	 * @param dir
	 *            the working directory
	 * @param processStreamHandler
	 *            process the streams (in, out, err) of the process
	 * @return the exit code of the process
	 * @throws IOException
	 *             executing the process failed
	 */
	private int executeInternal(final CommandLine command, final Map<String, String> environment, final File dir, final ProcessStreamHandler processStreamHandler) throws IOException {
		processStreamHandler.setScheduledExecutorService(this.getScheduledExecutorService());
		final Process process;
		exceptionCaught = null;

		try {
			process = this.launch(command, environment, dir);
		} catch (final IOException e) {
			LOGGER.debug("fail when launch proces", e);
			this.failedToStart(e);
			throw e;
		}
		LOGGER.info("Process started: {}", process);
		try {
			processStreamHandler.setProcessOutputStream(process.getOutputStream());
			processStreamHandler.setProcessInputStream(process.getInputStream());
			processStreamHandler.setProcessErrorStream(process.getErrorStream());
		} catch (final IOException e) {
			LOGGER.debug("fail when set proces streams", e);
			process.destroy();
			this.failedToStart(e);
			throw e;
		}
		
		processStreamHandler.start();

		try {

			// add the process to the list of those to destroy if the VM exits
			if (this.getProcessDestroyer() != null) {
				this.getProcessDestroyer().add(process);
			}

			// TODO
			// associate the watchdog with the newly created process
			/*
			 * if (watchdog != null) { watchdog.start(process); }
			 */
			
			int exitValue = Executor.INVALID_EXITVALUE;
			LOGGER.debug("process is waiting finish");
			try {
				exitValue = process.waitFor();
			} catch (final InterruptedException e) {
				LOGGER.debug("error when waitFor {}", e);
				process.destroy();
			} finally {
				// see http://bugs.sun.com/view_bug.do?bug_id=6420270
				// see https://issues.apache.org/jira/browse/EXEC-46
				// Process.waitFor should clear interrupt status when throwing
				// InterruptedException
				// but we have to do that manually
				Thread.interrupted();
			}
			LOGGER.debug("process is finished");
			// TODO
			/*
			 * if (watchdog != null) { watchdog.stop(); }
			 */

			try {
				processStreamHandler.stop();
			} catch (final IOException e) {
				LOGGER.debug("fail when stop threads", e);
				setExceptionCaught(e);
			}

			closeProcessStreams(process);

			if (getExceptionCaught() != null) {
				throw getExceptionCaught();
			}

			// TODO
			/*
			 * if (watchdog != null) { try { watchdog.checkException(); } catch (final
			 * IOException e) { throw e; } catch (final Exception e) { // Java 1.5 does not
			 * support public IOException(String message, Throwable cause) final IOException
			 * ioe = new IOException(e.getMessage()); ioe.initCause(e); throw ioe; } }
			 */

			if (this.isFailure(exitValue)) {
				throw new ExecuteException("Process exited with an error: " + exitValue, exitValue);
			}

			return exitValue;
		} finally {
			// remove the process to the list of those to destroy if the VM exits
			if (this.getProcessDestroyer() != null) {
				this.getProcessDestroyer().remove(process);
			}
		}
	}

	private void failedToStart(IOException e) {
		// TODO Auto-generated method stub
		
	}

	/**
	 * Keep track of the first IOException being thrown.
	 *
	 * @param e
	 *            the IOException
	 */
	private void setExceptionCaught(final IOException e) {
		if (this.exceptionCaught == null) {
			this.exceptionCaught = e;
		}
	}

	/**
	 * Get the first IOException being thrown.
	 *
	 * @return the first IOException being caught
	 */
	private IOException getExceptionCaught() {
		return this.exceptionCaught;
	}

	/**
	 * Use latest @see java.util.concurrent.ExecutorService
	 * 
	 * @param executorService
	 */
	protected void setScheduledExecutorService(final ScheduledExecutorService scheduledExecutorService) {
		if(scheduledExecutorService==null) {
			throw new java.lang.UnsupportedOperationException("ScheduledExecutorService must not be null");
		}
		
		if(scheduledExecutorService==this.scheduledExecutorService) {
			useInternalExecutorService = true;
		}
		else {
			useInternalExecutorService = false;
		}
		
		if(this.scheduledExecutorService!=null) {
			if(useInternalExecutorService) {
				scheduledExecutorService.shutdown();
			}
		}
		this.scheduledExecutorService = scheduledExecutorService;
	}

	protected ScheduledExecutorService getScheduledExecutorService() {
		if(this.scheduledExecutorService==null) {
			useInternalExecutorService = true;
			this.scheduledExecutorService = Executors.newScheduledThreadPool(4);
		}
		return this.scheduledExecutorService;
	}

	@Override
	public void addProcessListener(ProcessListener l) {
		listeners.add(ProcessListener.class, l);
	}

	@Override
	public void removeProcessListener(ProcessListener l) {
		listeners.remove(ProcessListener.class, l);
	}

	@Override
	public void clearProcessListeners() {
		listeners = new EventListenerList();
	}

}
